package top.wangjiu.reactive.rpc.spring.client;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.PropertyValues;
import org.springframework.beans.factory.*;
import org.springframework.beans.factory.annotation.InjectionMetadata;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.DependencyDescriptor;
import org.springframework.beans.factory.config.SmartInstantiationAwareBeanPostProcessor;
import org.springframework.core.BridgeMethodResolver;
import org.springframework.core.MethodParameter;
import org.springframework.core.Ordered;
import org.springframework.core.PriorityOrdered;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
import top.wangjiu.reactive.rpc.config.ClientConfiguration;
import top.wangjiu.reactive.rpc.config.ServiceConfig;
import top.wangjiu.reactive.rpc.generater.RequestIdGenerater;
import top.wangjiu.reactive.rpc.loadbalance.LoadBalance;
import top.wangjiu.reactive.rpc.util.RpcUtil;

import java.beans.PropertyDescriptor;
import java.lang.reflect.*;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @ClassName RpcClientAnnotationBeanPostProcessor
 * @Description 仿照 AutowiredAnnotationBeanPostProcessor 的逻辑
 * @Author wangjiu
 */
public class RpcClientAnnotationBeanPostProcessor implements SmartInstantiationAwareBeanPostProcessor,
        BeanPostProcessor, PriorityOrdered, BeanFactoryAware {

    protected final Log logger = LogFactory.getLog(getClass());

    //比autowired 先执行
    private int order = Ordered.LOWEST_PRECEDENCE - 3;

    @Nullable
    private ConfigurableListableBeanFactory beanFactory;

    private volatile ClientConfiguration clientConfiguration;

    private final Map<String, InjectionMetadata> injectionMetadataCache = new ConcurrentHashMap<>(256);

    @Override
    public int getOrder() {
        return order;
    }

    @Override
    public void setBeanFactory(BeanFactory beanFactory) {
        if (!(beanFactory instanceof ConfigurableListableBeanFactory)) {
            throw new IllegalArgumentException(
                    "AutowiredAnnotationBeanPostProcessor requires a ConfigurableListableBeanFactory: " + beanFactory);
        }
        this.beanFactory = (ConfigurableListableBeanFactory) beanFactory;
    }

    @Override
    public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) {
        InjectionMetadata metadata = findAutowiringMetadata(beanName, bean.getClass(), pvs);
        try {
            metadata.inject(bean, beanName, pvs);
        }
        catch (BeanCreationException ex) {
            throw ex;
        }
        catch (Throwable ex) {
            throw new BeanCreationException(beanName, "Injection of autowired dependencies failed", ex);
        }
        return pvs;
    }

    private InjectionMetadata findAutowiringMetadata(String beanName, Class<?> clazz, @Nullable PropertyValues pvs) {
        // Fall back to class name as cache key, for backwards compatibility with custom callers.
        String cacheKey = (StringUtils.hasLength(beanName) ? beanName : clazz.getName());
        // Quick check on the concurrent map first, with minimal locking.
        InjectionMetadata metadata = this.injectionMetadataCache.get(cacheKey);
        if (InjectionMetadata.needsRefresh(metadata, clazz)) {
            synchronized (this.injectionMetadataCache) {
                metadata = this.injectionMetadataCache.get(cacheKey);
                if (InjectionMetadata.needsRefresh(metadata, clazz)) {
                    if (metadata != null) {
                        metadata.clear(pvs);
                    }
                    metadata = buildAutowiringMetadata(clazz);
                    this.injectionMetadataCache.put(cacheKey, metadata);
                }
            }
        }
        return metadata;
    }


    private InjectionMetadata buildAutowiringMetadata(final Class<?> clazz) {
        if (!AnnotationUtils.isCandidateClass(clazz, RpcClient.class)) {
            return InjectionMetadata.EMPTY;
        }

        List<InjectionMetadata.InjectedElement> elements = new ArrayList<>();
        Class<?> targetClass = clazz;

        do {
            final List<InjectionMetadata.InjectedElement> currElements = new ArrayList<>();

            ReflectionUtils.doWithLocalFields(targetClass, field -> {
                RpcClient annotation = field.getAnnotation(RpcClient.class);
                if (annotation != null) {
                    if (Modifier.isStatic(field.getModifiers())) {
                        if (logger.isInfoEnabled()) {
                            logger.info("RpcClient annotation is not supported on static fields: " + field);
                        }
                        return;
                    }
                    currElements.add(new AutowiredFieldElement(field));
                }
            });

            ReflectionUtils.doWithLocalMethods(targetClass, method -> {
                Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(method);
                if (!BridgeMethodResolver.isVisibilityBridgeMethodPair(method, bridgedMethod)) {
                    return;
                }
                RpcClient annotation = method.getAnnotation(RpcClient.class);
                if (annotation != null && method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) {
                    if (Modifier.isStatic(method.getModifiers())) {
                        if (logger.isInfoEnabled()) {
                            logger.info("RpcClient annotation is not supported on static methods: " + method);
                        }
                        return;
                    }
                    if (method.getParameterCount() == 0) {
                        if (logger.isInfoEnabled()) {
                            logger.info("RpcClient annotation should only be used on methods with parameters: " +
                                    method);
                        }
                    }
                    PropertyDescriptor pd = BeanUtils.findPropertyForMethod(bridgedMethod, clazz);
                    currElements.add(new AutowiredMethodElement(method, pd));
                }
            });

            elements.addAll(0, currElements);
            targetClass = targetClass.getSuperclass();
        }
        while (targetClass != null && targetClass != Object.class);

        return InjectionMetadata.forElements(elements, clazz);
    }

    private void registerDependentBeans(@Nullable String beanName, String autowiredBeanName) {
        if (beanName != null) {
            if (this.beanFactory != null && this.beanFactory.containsBean(autowiredBeanName)) {
                this.beanFactory.registerDependentBean(beanName, autowiredBeanName);
            }
            if (logger.isTraceEnabled()) {
                logger.trace("RpcClient by type from bean name '" + beanName +
                        "' to bean named '" + autowiredBeanName + "'");
            }
        }
    }

    private ClientConfiguration getClientConfiguration(){
        if(clientConfiguration==null){
            clientConfiguration = beanFactory.getBean(ClientConfiguration.class);
        }
        return clientConfiguration;
    }

    private ServiceConfig buildServiceConfig(RpcClient annotation){
        LoadBalance loadBalance = getLoadBalance(annotation);

        RequestIdGenerater requestIdGenerater = getRequestIdGenerater(annotation);

        return AnnotationServiceConfig.builder()
                .version(annotation.version())
                .timeout(annotation.timeout())
                .loadBalance(loadBalance)
                .requestIdGenerater(requestIdGenerater)
                .build();
    }

    private LoadBalance getLoadBalance(RpcClient annotation){
        LoadBalance loadBalance = null;
        String balanceBeanName = annotation.loadBalanceBeanName();
        if(!balanceBeanName.isEmpty()){
            loadBalance = getBean(balanceBeanName, LoadBalance.class);
        }
        if(loadBalance == null){
            try {
                loadBalance = annotation.loadBalanceType().getConstructor().newInstance();
            } catch (Exception e) {
                logger.error("constructe loadbalance error",e);
                throw new RuntimeException("constructe loadbalance error",e);
            }
        }
        return loadBalance;
    }

    private RequestIdGenerater getRequestIdGenerater(RpcClient annotation){
        RequestIdGenerater generater = null;
        String beanName = annotation.requestIdGeneraterBeanName();
        if(!beanName.isEmpty()){
            generater = getBean(beanName, RequestIdGenerater.class);
        }
        if(generater == null){
            try {
                generater = annotation.requestIdGeneraterType().getConstructor().newInstance();
            } catch (Exception e) {
                logger.error("constructe loadbalance error",e);
                throw new RuntimeException("constructe loadbalance error",e);
            }
        }
        return generater;
    }

    private <T> T getBean(String beanName,Class<T> requiredType){
        T bean;
        try {
            bean = beanFactory.getBean(beanName, requiredType);
        }catch (Exception e){
            bean = null;
        }
        return bean;
    }


    private <T> T getRpcClientInstance(RpcClient annotation,Class<T> clientType,String beanName){
        String clientName = ClientBeanNameBuilder.builder(annotation, clientType);
        T value;
        try {
            value = beanFactory.getBean(clientName,clientType);
        }catch (Exception e){
            ClientConfiguration configuration = getClientConfiguration();
            ServiceConfig serviceConfig = buildServiceConfig(annotation);
            value = RpcUtil.getRpcInstance(clientType,serviceConfig,configuration);
            beanFactory.registerSingleton(clientName,value);
        }
        registerDependentBeans(beanName, clientName);
        return value;
    }

    private class AutowiredFieldElement extends InjectionMetadata.InjectedElement {

        public AutowiredFieldElement(Field field) {
            super(field, null);
        }

        @Override
        protected void inject(Object bean, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {
            Field field = (Field) this.member;
            Object value = resolveFieldValue(field, bean, beanName);
            if (value != null) {
                ReflectionUtils.makeAccessible(field);
                field.set(bean, value);
            }
        }

        @Nullable
        private Object resolveFieldValue(Field field, Object bean, @Nullable String beanName) {
            DependencyDescriptor desc = new DependencyDescriptor(field, true);
            desc.setContainingClass(bean.getClass());
            RpcClient annotation = field.getAnnotation(RpcClient.class);
            return getRpcClientInstance(annotation,field.getType(),beanName);
        }
    }

    private class AutowiredMethodElement extends InjectionMetadata.InjectedElement {

        public AutowiredMethodElement(Method method, @Nullable PropertyDescriptor pd) {
            super(method, pd);
        }

        @Override
        protected void inject(Object bean, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {
            if (checkPropertySkipping(pvs)) {
                return;
            }
            Method method = (Method) this.member;
            Object[] arguments = resolveMethodArguments(method, bean, beanName);
            if (arguments != null) {
                try {
                    ReflectionUtils.makeAccessible(method);
                    method.invoke(bean, arguments);
                }
                catch (InvocationTargetException ex) {
                    throw ex.getTargetException();
                }
            }
        }

        @Nullable
        private Object[] resolveMethodArguments(Method method, Object bean, @Nullable String beanName) {
            int argumentCount = method.getParameterCount();
            Object[] arguments = new Object[argumentCount];
            Assert.state(beanFactory != null, "No BeanFactory available");
            RpcClient annotation = method.getAnnotation(RpcClient.class);
            for (int i = 0; i < arguments.length; i++) {
                MethodParameter methodParam = new MethodParameter(method, i);
                arguments[i] = getRpcClientInstance(annotation,methodParam.getParameterType(),beanName);
            }
            return arguments;
        }
    }

}
